﻿using BoardGame;
using GameModelView;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;


namespace GameModel
{
    /// <summary>
    /// Represent and mediate a chess game.
    /// </summary>
    /// <remarks>
    /// This class is intended to be visible to the Controller in the MVC pattern.
    /// </remarks>
    public class ChessModel : ChessModelView
    {
        public ChessModel ()
        {
            SetupStandard ();
        }

        /// <summary>
        /// Create a board with kings only.  More tokens may be added later.
        /// </summary>
        public ChessModel (int whiteKingX, int whiteKingY, int blackKingX, int blackKingY)
        {
            if (whiteKingX < 0 || whiteKingX >= Board.XSize)
                throw new ArgumentOutOfRangeException ("whiteKingX");

            if (whiteKingY < 0 || whiteKingY >= Board.YSize)
                throw new ArgumentOutOfRangeException ("whiteKingY");

            if (blackKingX < 0 || blackKingX >= Board.XSize)
                throw new ArgumentOutOfRangeException ("blackKingX");

            if (blackKingY < 0 || blackKingY >= Board.YSize)
                throw new ArgumentOutOfRangeException ("blackKingY");

            Setup (new King (Player.Black), blackKingX, blackKingY);
            Setup (new King (Player.White), whiteKingX, whiteKingY);
        }


        /// <summary>Create a board with standard setup.</summary>
        public void SetupStandard ()
        {
            Clear ();

            Setup (new King (Player.White), 4, 0);
            Setup (new Rook (Player.White), 0, 0);
            Setup (new Knight (Player.White), 1, 0);
            Setup (new Bishop (Player.White), 2, 0);
            Setup (new Queen (Player.White), 3, 0);
            Setup (new Bishop (Player.White), 5, 0);
            Setup (new Knight (Player.White), 6, 0);
            Setup (new Rook (Player.White), 7, 0);

            for (int x = 0; x < Board.XSize; ++x)
                Setup (new Pawn (Player.White), x, 1);

            for (int x = 0; x < Board.XSize; ++x)
                for (int y = 0; y < Board.YSize / 2; ++y)
                {
                    Piece piece = Board[x, y] as Piece;
                    if (piece != null)
                    {
                        Piece piece2 = piece.MakeOpposite ();
                        Setup (piece2, x, Board.YSize - y - 1);
                    }
                }

            firstStatus = GameStatus.Go;

            OnGameCreated (this);
            OnStatusUpdate ();
        }


        /// <summary>Place a piece on the board.</summary>
        /// <param name="piece">Token to place.</param>
        /// <param name="x">Initial X coordinate of piece.</param>
        /// <param name="y">Initial Y coordinate of piece.</param>
        public void Setup (Piece piece, int x, int y)
        {
            if (Plies.Count > 0)
                throw new InvalidOperationException ("Game is underway");

            if (x < 0 || x >= Board.XSize)
                throw new ArgumentOutOfRangeException ("x");

            if (y < 0 || y >= Board.YSize)
                throw new ArgumentOutOfRangeException ("y");

            if (Board[x, y].IsOccupied)
                throw new ArgumentException ("Coordinate already occupied");

            if (piece.Game != null)
                throw new ArgumentException ("Piece already placed", "piece");

            piece.Game = (ChessModelView) this;
            piece.At = new Coordinate (x, y);
            Board.squares[x, y] = piece;
            Pieces.Add (piece);
        }


        /// <summary>Assign promotion piece for next time pawn is promoted.
        /// </summary>
        /// <param name="pieceName">Name of promotion piece.</param>
        public void SetPromotion (string pieceName)
        {
            Player player = PlayerTurn;

            if (pieceName.Equals (typeof (Queen).Name, StringComparison.OrdinalIgnoreCase))
                SetPromotion (player, typeof (Queen));
            else if (pieceName.Equals (typeof (Rook).Name, StringComparison.OrdinalIgnoreCase))
                SetPromotion (player, typeof (Rook));
            else if (pieceName.Equals (typeof (Bishop).Name, StringComparison.OrdinalIgnoreCase))
                SetPromotion (player, typeof (Bishop));
            else if (pieceName.Equals (typeof (Knight).Name, StringComparison.OrdinalIgnoreCase))
                SetPromotion (player, typeof (Knight));
            else
                throw new ArgumentException ("Unknown promotion '" + pieceName + "'");
        }


        /// <summary>Assign promotion piece for next time pawn is promoted.
        /// </summary>
        /// <param name="player">Player of promotion assignment.</param>
        /// <param name="pieceType">Type of promotion piece.</param>
        public void SetPromotion (Player player, Type pieceType)
        {
            if (pieceType != typeof (Queen)
                && pieceType != typeof (Rook)
                && pieceType != typeof (Bishop)
                && pieceType != typeof (Knight))
                throw new ArgumentException ("Invalid promotion piece");

            Promotions[(int) player] = pieceType;
        }


        public void MakeMove (int fromX, int fromY, int toX, int toY)
        {
            if (Plies.Count == 0)
                if (firstStatus == GameStatus.Setup)
                {
                    if (IsKingThreatened (Player.Black))
                        throw new InvalidOperationException ("Cannot begin with opposing king threat");

                    firstStatus = CalcStatus ();
                }

            if (!IsUnderway)
                throw new InvalidOperationException ("Cannot move piece when game is complete");

            if (fromX < 0 || fromY < 0 || toX >= Board.XSize || toY >= Board.YSize)
                throw new ArgumentException ("Coordinate out of bounds");

            Piece piece = Board[fromX, fromY] as Piece;
            if (piece == null || piece.Side != PlayerTurn)
                throw new InvalidOperationException ("Square not occupied by current player");

            if (TryPushMove (fromX, fromY, toX, toY))
                throw new InvalidOperationException ("Illegal move");

            Ply ply = Plies.Top;
            ply.PlyStatus = CalcStatus ();
            Same6Moves = IsSame6Moves ();
            Is50MoveRuleClaimable = GetIs50MoveRuleClaimable ();

            OnTokenRemove (fromX, fromY);

            if (ply.IsPawnPromotion)
                OnTokenPlace (toX, toY, ply.Moving.Side, ply.Promotion.Token);
            else
            {
                OnTokenPlace (toX, toY, ply.Moving.Side, ply.Moving.Token);
                if (ply.IsCaptureEnPassant)
                    OnTokenRemove (toX, fromY);
                else if (ply.IsQueensideCastle)
                {
                    OnTokenRemove (0, fromY);
                    OnTokenPlace (toX + 1, toY, ply.Moving.Side, Token.Rook);
                }
                else if (ply.IsKingsideCastle)
                {
                    OnTokenRemove (Board.YSize - 1, fromY);
                    OnTokenPlace (toX - 1, toY, ply.Moving.Side, Token.Rook);
                }
            }

            OnPieceMoved ();
            OnStatusUpdate ();
        }


        // Returns true if illegal move, false if legal.
        private bool TryPushMove (int fromX, int fromY, int toX, int toY)
        {
            Piece piece = Board[fromX, fromY] as Piece;

            Debug.Assert (piece != null || piece.Side == PlayerTurn);

            foreach (Coordinate move in piece.GetLegalMoves())
                if (move.X == toX && move.Y == toY)
                {
                    Plies.Push (piece, toX, toY);
                    return false;
                }

            return true;
        }


        public void UndoMove ()
        {
            if (!IsUnderway || Plies.Count == 0)
                throw new InvalidOperationException ("Nothing to undo");

            Ply ply = Plies.Top;
            Player player = ply.Moving.Side;

            if (ply.Captured == null)
                OnTokenRemove (ply.To.X, ply.To.Y);
            else
            {
                int y;
                if (ply.IsCaptureEnPassant)
                {
                    y = ply.From.Y;
                    OnTokenRemove (ply.To.X, ply.To.Y);
                }
                else
                    y = ply.To.Y;

                OnTokenPlace (ply.To.X, y, ply.Captured.Side, ply.Captured.Token);
            }

            Token token;
            if (ply.Promotion != null)
                token = Token.Pawn;
            else
                token = ply.Moving.Token;

            OnTokenPlace (ply.From.X, ply.From.Y, player, token);

            if (ply.Moving is King)
            {
                if (ply.IsQueensideCastle)
                {
                    OnTokenPlace (0, ply.To.Y, player, Token.Rook);
                    OnTokenRemove (ply.To.X + 1, ply.To.Y);
                }
                else if (ply.IsKingsideCastle)
                {
                    OnTokenPlace (Board.XSize - 1, ply.To.Y, player, Token.Rook);
                    OnTokenRemove (ply.To.X - 1, ply.To.Y);
                }
            }

            OnUndoPreview ();
            Plies.Pop ();
            Same6Moves = IsSame6Moves ();
            Is50MoveRuleClaimable = GetIs50MoveRuleClaimable();
            OnStatusUpdate ();
        }


        public void DrawGame ()
        {
            if (!IsUnderway)
                throw new InvalidOperationException ("Cannot draw a completed game.");

            SetStatus (Status | GameStatus.Tie);
            OnStatusUpdate ();
        }


        public void ResignGame ()
        {
            if (!IsUnderway)
                throw new InvalidOperationException ("Cannot resign a completed game.");

            SetStatus (Status | GameStatus.Loss | GameStatus.Resign);
            OnStatusUpdate ();
        }


        public void BeginShowMoves (int x, int y)
        {
            OnHilitesClear ();

            Piece piece = Board[x, y] as Piece;
            if (piece != null)
                foreach (Coordinate moveTo in piece.GetLegalMoves())
                    OnHiliteChange (moveTo.X, moveTo.Y, true);
        }


        public void EndShowMoves ()
        {
            OnHilitesClear ();
        }
    }
}
